namespace ScrumSprintMonitor.Implementation
{
	#region #using Directives

	using System;
	using System.Collections.Generic;
	using System.ComponentModel;
	using System.Diagnostics;
	using System.Linq;
	using System.Threading;

	using AvailableTimeCalculator;

	using Infrastructure;
	using Infrastructure.Configuration;

	using Ninject.Core;
	using Ninject.Core.Parameters;

	#endregion

	public class Sprint : ISprint,
	                      IDisposable
	{
		#region Private Fields

		private readonly ITeamMemberAvailableTimeCalculator availableTimeCalculator;
		private readonly IConnection connection;
		private readonly IKernel kernel;
		private readonly ILogger<Sprint> logger;
		private readonly IShellLayoutConfiguration shellLayoutConfiguration;
		private readonly ITeamMemberAvailableTimeCalculator simpleAvailableTimeCalculator;
		private readonly ISprintConfiguration sprintConfiguration;

		private readonly SynchronizationContext syncContext;
		private readonly ITeamMemberInfoProvider teamInfoProvider;
		private readonly BindingList<ITeamMember> teamMembersList = new BindingList<ITeamMember>();
		private readonly Timer timer;
		private readonly IWorkItemInterpretationConfiguration workItemInterpretationConfiguration;
		private readonly TimeSpan WorkItemQueryLifetime = TimeSpan.FromMinutes(1.0);

		private TimeSpan availableTime;
		private bool disposed;
		private Exception exception;
		private DateTime? lastWorkItemsRefresh, lastWorkItemsUpdate;
		private string name;
		private bool notAllWorkItemsEstimated;
		private SprintStatus status = SprintStatus.NoDataAvailable;
		private bool teamInfoProviderSubscribed;
		private bool timeIncludesFreeBusyInformation;
		private TimeSpan totalTime;
		private ITeamMember unassignedTeamMember;
		private ITeamMember unassignedWork;
		private IWorkItemStore workItemStore;

		#endregion

		public Sprint(IKernel kernel, IConnection connection, ISprintConfiguration sprintConfiguration, IShellLayoutConfiguration shellLayoutConfiguration,
		              IWorkItemInterpretationConfiguration workItemInterpretationConfiguration, ITeamMemberAvailableTimeCalculator availableTimeCalculator,
		              ITeamMemberInfoProvider teamInfoProvider, ILogger<Sprint> logger)
		{
			if (sprintConfiguration.SprintEndDate < sprintConfiguration.SprintStartDate)
			{
				throw new ArgumentException("Sprint dates are incorrect.", "sprintConfiguration");
			}

			this.kernel = kernel;
			this.shellLayoutConfiguration = shellLayoutConfiguration;
			this.connection = connection;
			this.sprintConfiguration = sprintConfiguration;
			this.workItemInterpretationConfiguration = workItemInterpretationConfiguration;
			this.availableTimeCalculator = availableTimeCalculator;
			this.simpleAvailableTimeCalculator = new SimpleTeamMemberAvailableTimeCalculator();
			this.teamInfoProvider = teamInfoProvider;
			this.logger = logger;

			this.syncContext = SynchronizationContext.Current;

			if (this.syncContext != null)
			{
				this.timer = new Timer(OnTimer, null, 1000, Timeout.Infinite);
			}
		}

		#region Private Methods/Properties

		private bool WorkItemQueryOutdated
		{
			get { return !LastRefresh.HasValue || (SystemTime.Now() - LastRefresh.Value) >= this.WorkItemQueryLifetime; }
		}

		private void GuardDisposed()
		{
			if (this.disposed)
			{
				throw new ObjectDisposedException("Sprint");
			}
		}

		/// <summary>
		/// This method redistributes unassigned work through all the underassigned team members.
		/// </summary>
		private static void RedistributeUnassignedWorkItems(ITeamMember unassignedTeamMember, ICollection<ITeamMember> teamMembersList,
		                                                    TimeSpan availableTime)
		{
			if (unassignedTeamMember != null)
			{
				var averageAvailableTime = TimeSpan.FromTicks(availableTime.Ticks / teamMembersList.Count);
				double averageUnassignedWork = (unassignedTeamMember.RemainingWorkload.SprintWork / teamMembersList.Count);
				var dedicatedTeamMembers = teamMembersList.Where(m => m.Information.DedicatedToSprint);
				var underassignedTeamMembers =
					dedicatedTeamMembers.Where(m => m.RemainingWorkload.SprintWork + averageUnassignedWork < averageAvailableTime.TotalHours);
				int underassignedTeamMemberCount = underassignedTeamMembers.Count();

				if (underassignedTeamMemberCount <= 1)
				{
					underassignedTeamMembers = dedicatedTeamMembers;
					underassignedTeamMemberCount = dedicatedTeamMembers.Count();
				}

				double unassignedWorkloadPerTeamMember = (unassignedTeamMember.RemainingWorkload.SprintWork / underassignedTeamMemberCount);
				Parallel.ForEach(underassignedTeamMembers, teamMember => teamMember.RemainingWorkload.UnassignedSprintWork = unassignedWorkloadPerTeamMember);
				unassignedTeamMember.RemainingWorkload.PotentialSprintHours =
					dedicatedTeamMembers.AsParallel().Sum(m => m.RemainingWorkload.PotentialSprintHours - m.RemainingWorkload.SprintWork);
			}
		}

		private void CalculateAvailableTimeForSprintFrom(IEnumerable<ITeamMember> teamMembers)
		{
			AvailableTime = TimeSpan.FromHours(teamMembers.Sum(m => m.RemainingWorkload.SprintHours));
			TimeIncludesFreeBusyInformation = !teamMembers.Any(m => !m.RemainingWorkload.IncludesFreeBusyInformation);
		}

		private void ProcessWorkItems()
		{
			this.logger.Info("Processing work item store...");

			try
			{
				var workItems = this.workItemStore.WorkItems;

				var unestimatedWorkItem = workItems.Where(x => x.Type == WorkItemType.SprintBacklogItem).FirstOrDefault(x => double.IsNaN(x.RemainingWork));
				NotAllWorkItemsEstimated = (unestimatedWorkItem != null);
				if (NotAllWorkItemsEstimated && !this.workItemInterpretationConfiguration.AllowUnestimatedWorkItems)
				{
					throw new RemainingWorkNotEstimatedException(unestimatedWorkItem);
				}

				var tempUnassignedWork = this.unassignedTeamMember;

				ExtractTeamMembersFromWorkItems(ref tempUnassignedWork);

				UpdateTimeCalculations(tempUnassignedWork);

				UnassignedWork = (tempUnassignedWork != null && tempUnassignedWork.RemainingWorkload.SprintWork > 0.0 ? tempUnassignedWork : null);

				LastUpdate = SystemTime.Now();
				Status = SprintStatus.OK;

				this.logger.Info("Processed work item store.");
			}
			catch (AggregateException ae)
			{
				this.logger.Error(ae, "Exception detected while processing sprint work items.");

				Exception = ae.InnerExceptions[0];
			}
			catch (RemainingWorkNotEstimatedException rwnee)
			{
				this.logger.Error(rwnee, "Unestimated work item detected while processing sprint work items.");

				Exception = rwnee;
			}
			catch (Exception e)
			{
				this.logger.Error(e, "Exception detected while processing sprint work items.");

				Exception = e;
			}
		}

		private void UpdateTimeCalculations()
		{
			UpdateTimeCalculations(UnassignedWork);
		}

		private void UpdateTimeCalculations(ITeamMember tempUnassignedWork)
		{
			this.logger.Info("Updating time calculations...");

			try
			{
				if (this.teamMembersList.Count > 0)
				{
					if (this.availableTimeCalculator is SimpleTeamMemberAvailableTimeCalculator)
					{
						TotalTime = TimeSpan.FromHours(this.teamMembersList.AsParallel().Sum(m => GetTotalHoursForTeamMember(m)));
						this.teamMembersList.AsParallel().ForAll(CalculateAvailableTimeForTeamMember);
					}
					else
					{
						TotalTime = TimeSpan.FromHours(this.teamMembersList.Sum(m => GetTotalHoursForTeamMember(m)));

						foreach (var teamMember in this.teamMembersList)
						{
							CalculateAvailableTimeForTeamMember(teamMember);
						}
					}
				}
				if (tempUnassignedWork != null)
				{
					CalculateAvailableTimeForSprintFrom(this.teamMembersList);
					RedistributeUnassignedWorkItems(tempUnassignedWork, this.teamMembersList, AvailableTime);
				}
				CalculateAvailableTimeForSprintFrom(this.teamMembersList); // Recalculate available time after capping non-dedicated team members' time

				if (this.unassignedTeamMember == null && tempUnassignedWork != null)
				{
					this.unassignedTeamMember = tempUnassignedWork;
				}
			}
			catch (ConnectionException ce)
			{
				this.logger.Error(ce, "Exception detected while updating available time calculations.");

				Exception = ce;

				return;
			}

			this.logger.Info("Updated time calculations.");
		}

		private void ExtractTeamMembersFromWorkItems(ref ITeamMember unassignedTeamMember)
		{
			if (this.workItemStore == null)
			{
				TeamMembers.Clear();
				unassignedTeamMember = null;
				return;
			}

			var sprintBacklogItems = this.workItemStore.WorkItems.Where(w => w.Type == WorkItemType.SprintBacklogItem);

			switch (this.shellLayoutConfiguration.ActiveSprintMode)
			{
				case SprintMode.Planning:
					if (unassignedTeamMember == null)
					{
						unassignedTeamMember = CreateTeamMember(null, sprintBacklogItems);
					}
					else
					{
						unassignedTeamMember.AssignedWorkItems = GetTeamMemberBacklogItems(null, sprintBacklogItems);
					}

					foreach (var teamMemberInfo in TeamInfoProvider.TeamMembers)
					{
						string teamMemberName = teamMemberInfo.Name;

						var teamMember = this.teamMembersList.FirstOrDefault(m => m.Information.Name == teamMemberName);
						if (teamMember == null)
						{
							teamMember = CreateTeamMember(teamMemberName, sprintBacklogItems);
							this.teamMembersList.Add(teamMember);
						}
						else
						{
							teamMember.AssignedWorkItems = GetTeamMemberBacklogItems(teamMember.Information.Name, sprintBacklogItems);
						}
					}
					break;
				case SprintMode.Sprinting:
				{
					var teamMemberNames = sprintBacklogItems.AsParallel().AsUnordered().Select(x => x.AssignedTo).Distinct().ToArray();

					// Add any team members which do not exist in the current list
					foreach (string teamMemberName in teamMemberNames)
					{
						bool isWorkItemUnassigned = string.IsNullOrEmpty(teamMemberName);

						if (isWorkItemUnassigned)
						{
							if (unassignedTeamMember == null)
							{
								unassignedTeamMember = CreateTeamMember(teamMemberName, sprintBacklogItems);
							}
						}
						else
						{
							string name = teamMemberName;
							if (!this.teamMembersList.Any(x => x.Information.Name == name))
							{
								this.teamMembersList.Add(CreateTeamMember(teamMemberName, sprintBacklogItems));
							}
						}
					}

					// Remove any team members in the current list that no longer belong to the sprint
					List<ITeamMember> invalidTeamMembersList = null;
					foreach (var teamMember in this.teamMembersList)
					{
						bool isWorkItemUnassigned = string.IsNullOrEmpty(teamMember.DisplayName);

						var member = teamMember;
						if (!teamMemberNames.Any(x => x == member.Information.Name))
						{
							if (isWorkItemUnassigned)
							{
								unassignedTeamMember = null;
							}
							else
							{
								if (invalidTeamMembersList == null)
								{
									invalidTeamMembersList = new List<ITeamMember>();
								}
								invalidTeamMembersList.Add(teamMember);
							}
						}
					}

					if (invalidTeamMembersList != null)
					{
						invalidTeamMembersList.ForEach(x => this.teamMembersList.Remove(x));
					}

					// Update any remaining team members in the current list
					if (unassignedTeamMember != null)
					{
						unassignedTeamMember.AssignedWorkItems = GetTeamMemberBacklogItems(null, sprintBacklogItems);
					}
					Parallel.ForEach(this.teamMembersList,
					                 delegate(ITeamMember teamMember) { teamMember.AssignedWorkItems = GetTeamMemberBacklogItems(teamMember.Information.Name, sprintBacklogItems); });

					break;
				}
			}
		}

		private ITeamMember CreateTeamMember(string teamMemberName, IEnumerable<IWorkItemAdapter> workItems)
		{
			bool isWorkItemUnassigned = string.IsNullOrEmpty(teamMemberName);
			var teamMemberInformation = TeamInfoProvider.GetTeamMemberInformation(isWorkItemUnassigned ? null : teamMemberName);

			if (teamMemberInformation == null)
			{
				throw new TeamMemberInformationNotFoundException(isWorkItemUnassigned ? null : teamMemberName);
			}

			var parameters = new Dictionary<string, object>
			                 	{
			                 		{"teamMemberInfo", teamMemberInformation},
			                 		{"assignedWorkItems", GetTeamMemberBacklogItems(teamMemberName, workItems)}
			                 	};
			var teamMember = this.kernel.Get<ITeamMember>(With.Parameters.ConstructorArguments(parameters));

			return teamMember;
		}

		private static IEnumerable<IWorkItemAdapter> GetTeamMemberBacklogItems(string teamMemberName, IEnumerable<IWorkItemAdapter> sprintBacklogItems)
		{
			bool isWorkItemUnassigned = string.IsNullOrEmpty(teamMemberName);

			return sprintBacklogItems.Where(x => isWorkItemUnassigned ? string.IsNullOrEmpty(x.AssignedTo) : x.AssignedTo == teamMemberName);
		}

		private double GetTotalHoursForTeamMember(ITeamMember teamMember)
		{
			double teamMemberTotalTime;

			try
			{
				teamMemberTotalTime = GetTeamMemberTotalTime(teamMember, this.availableTimeCalculator);
			}
			catch (ConnectionException ce)
			{
				this.logger.Warn(ce, "Exception detected while calculating total hours. Reverting to simple calculation for team member {0}.",
				                 teamMember.DisplayName);

				teamMemberTotalTime = GetTeamMemberTotalTime(teamMember, this.simpleAvailableTimeCalculator);
			}

			return teamMemberTotalTime * teamMember.Information.HourlyProductivityFactor;
		}

		private void CalculateAvailableTimeForTeamMember(ITeamMember teamMember)
		{
			double teamMemberAvailableHours;

			try
			{
				teamMemberAvailableHours = GetTeamMemberAvailableHours(teamMember, this.availableTimeCalculator);
			}
			catch (ConnectionException ce)
			{
				this.logger.Warn(ce, "Exception detected while calculating available hours. Reverting to simple calculation for team member {0}.",
				                 teamMember.DisplayName);

				teamMemberAvailableHours = GetTeamMemberAvailableHours(teamMember, this.simpleAvailableTimeCalculator);
			}

			teamMember.RemainingWorkload.PotentialSprintHours = teamMemberAvailableHours * teamMember.Information.HourlyProductivityFactor;
		}

		private double GetTeamMemberAvailableHours(ITeamMember teamMember, ITeamMemberAvailableTimeCalculator timeCalculator)
		{
			var startTimeStamp = SystemTime.Now();

			// Make sure that the start time is not previous to the sprint start
			if (startTimeStamp < Start)
			{
				startTimeStamp = Start;
			}

			double sprintHours = timeCalculator.GetAvailableHours(teamMember.Information, startTimeStamp, End, TimeCalculationOptions.FutureOnly);
			teamMember.RemainingWorkload.IncludesFreeBusyInformation = timeCalculator.HasFreeBusyInformationFor(teamMember.Information);
			return sprintHours;
		}

		private double GetTeamMemberTotalTime(ITeamMember teamMember, ITeamMemberAvailableTimeCalculator timeCalculator)
		{
			return timeCalculator.GetAvailableHours(teamMember.Information, Start, End, TimeCalculationOptions.PastAndFuture);
		}

		private void OnTimer(object state)
		{
			if (this.syncContext != null)
			{
				this.syncContext.Post(RefreshData, null);
			}
		}

		private void RefreshData(object state)
		{
			if (this.disposed)
			{
				return;
			}

			try
			{
				RefreshData(true, false);
			}
			finally
			{
				if (this.timer != null && !this.disposed)
				{
					this.timer.Change(60000, Timeout.Infinite);
				}
			}
		}

		private void RefreshData(bool refreshWorkItems, bool forceRefresh)
		{
			try
			{
				GuardDisposed();

				if (LastRefresh.HasValue && LastUpdate.HasValue)
				{
					if ((LastRefresh.Value - LastUpdate.Value) >= this.WorkItemQueryLifetime)
					{
						// If no work items have been updated in the last query, at least update
						// the available time calculations
						UpdateTimeCalculations();
					}
				}

				if (forceRefresh || !refreshWorkItems || WorkItemQueryOutdated)
				{
					this.logger.Info("Requesting work item store refresh...");

					try
					{
						if (!this.connection.IsConnected)
						{
							this.connection.Connect();

							if (this.disposed)
							{
								return;
							}
						}

						if (forceRefresh || refreshWorkItems || this.workItemStore == null)
						{
							bool initialGet = (WorkItemStore == null);

							if (initialGet)
							{
								WorkItemStore = this.connection.GetWorkItemStore(this.sprintConfiguration);
							}

							if (WorkItemStore != null)
							{
								if (refreshWorkItems)
								{
									WorkItemStore.Refresh();

									if (this.disposed) // WorkItemStore.Refresh may run its own message pump. Check if we have been disposed.
									{
										return;
									}
								}
								else
								{
									ProcessWorkItems();
								}
							}
						}
					}
					catch (ConnectionException ce)
					{
						this.logger.Error(ce, "Exception detected while requesting refresh of sprint work items.");

						Exception = ce;
					}
					catch (ArgumentException e)
					{
						this.logger.Error(e, "Exception detected while requesting refresh of sprint work items.");

						Exception = e;
					}
					catch (Exception e)
					{
						this.logger.Error(e, "Exception detected while requesting refresh of sprint work items.");

						Exception = e;
					}
				}
			}
			catch (Exception e)
			{
				this.logger.Error(e, "Unexpected exception detected in RefreshData.");

				throw;
			}

			this.logger.Info("Work item store refresh requested.");
		}

		#endregion

		#region Implementation of ISprint

		private ITeamMemberInfoProvider TeamInfoProvider
		{
			get
			{
				if (!this.teamInfoProviderSubscribed)
				{
					this.teamInfoProvider.PropertyChanged += teamInfoProvider_PropertyChanged;
					this.teamInfoProviderSubscribed = true;
				}

				return this.teamInfoProvider;
			}
		}

		public SprintStatus Status
		{
			get
			{
				GuardDisposed();

				return this.status;
			}
			private set
			{
				GuardDisposed();

				if (value != this.status)
				{
					this.status = value;
					AsyncRaisePropertyChanged("Status");
				}
			}
		}

		public Exception Exception
		{
			get
			{
				GuardDisposed();

				return this.exception;
			}
			private set
			{
				GuardDisposed();

				if (value != this.exception)
				{
					this.exception = value;

					this.logger.Info(value, "Exception assigned to ISprint.Exception.");

					AsyncRaisePropertyChanged("Exception");
				}
			}
		}

		/// <summary>
		/// Name of the sprint
		/// </summary>
		public string Name
		{
			get
			{
				GuardDisposed();

				if (!this.connection.IsConnected)
				{
					this.connection.Connect();
				}

				this.name = this.connection.GetSprintName(this.sprintConfiguration);

				return this.name;
			}
		}

		/// <summary>
		/// Timestamp of the start of the sprint
		/// </summary>
		public DateTime Start
		{
			get
			{
				GuardDisposed();

				return this.sprintConfiguration.GetLocalSprintStartDate();
			}
		}

		/// <summary>
		/// Timestamp of the scheduled end of the sprint
		/// </summary>
		public DateTime End
		{
			get
			{
				GuardDisposed();

				return this.sprintConfiguration.GetLocalSprintEndDate();
			}
		}

		public IWorkItemStore WorkItemStore
		{
			get
			{
				GuardDisposed();

				return this.workItemStore;
			}
			private set
			{
				GuardDisposed();

				if (value != this.workItemStore)
				{
					if (this.workItemStore != null)
					{
						this.workItemStore.WorkItemsUpdated -= workItemStore_WorkItemsUpdated;
						this.workItemStore.WorkItemsRefreshed -= workItemStore_WorkItemsRefreshed;
					}

					this.workItemStore = value;
					AsyncRaisePropertyChanged("WorkItemStore");

					if (this.workItemStore != null)
					{
						this.workItemStore.WorkItemsUpdated += workItemStore_WorkItemsUpdated;
						this.workItemStore.WorkItemsRefreshed += workItemStore_WorkItemsRefreshed;
					}
				}
			}
		}

		/// <summary>
		/// Sum of the man-hours available to complete this sprint
		/// </summary>
		public TimeSpan AvailableTime
		{
			get
			{
				GuardDisposed();

				return this.availableTime;
			}
			private set
			{
				GuardDisposed();

				if (value != this.availableTime)
				{
					this.availableTime = value;
					AsyncRaisePropertyChanged("AvailableTime");
				}
			}
		}

		public TimeSpan TotalTime
		{
			get
			{
				GuardDisposed();

				return this.totalTime;
			}
			private set
			{
				GuardDisposed();

				if (value != this.totalTime)
				{
					this.totalTime = value;
					AsyncRaisePropertyChanged("TotalTime");
				}
			}
		}

		public bool TimeIncludesFreeBusyInformation
		{
			get
			{
				GuardDisposed();

				return this.timeIncludesFreeBusyInformation;
			}
			private set
			{
				GuardDisposed();

				if (value != this.timeIncludesFreeBusyInformation)
				{
					this.timeIncludesFreeBusyInformation = value;
					AsyncRaisePropertyChanged("TimeIncludesFreeBusyInformation");
				}
			}
		}

		public bool NotAllWorkItemsEstimated
		{
			get
			{
				GuardDisposed();

				return this.notAllWorkItemsEstimated;
			}
			private set
			{
				GuardDisposed();

				if (value != this.notAllWorkItemsEstimated)
				{
					this.notAllWorkItemsEstimated = value;
					AsyncRaisePropertyChanged("NotAllWorkItemsEstimated");
				}
			}
		}

		/// <summary>
		/// Team members with sprint backlog items assigned to them
		/// </summary>
		public IList<ITeamMember> TeamMembers
		{
			get
			{
				GuardDisposed();

				return this.teamMembersList;
			}
		}

		public ITeamMember UnassignedWork
		{
			get
			{
				GuardDisposed();

				return this.unassignedWork;
			}
			private set
			{
				if (value != this.unassignedWork)
				{
					this.unassignedWork = value;
					AsyncRaisePropertyChanged("UnassignedWork");
				}
			}
		}

		public void Refresh(bool forceRefresh)
		{
			RefreshData(true, forceRefresh);
		}

		/// <summary>
		/// Timestamp of the last successful update
		/// </summary>
		public DateTime? LastRefresh
		{
			get
			{
				GuardDisposed();

				return this.lastWorkItemsRefresh;
			}
			private set
			{
				GuardDisposed();

				if (value != this.lastWorkItemsRefresh)
				{
					this.lastWorkItemsRefresh = value;
					AsyncRaisePropertyChanged("LastRefresh");
				}
			}
		}

		public DateTime? LastUpdate
		{
			get
			{
				GuardDisposed();

				return this.lastWorkItemsUpdate;
			}
			private set
			{
				GuardDisposed();

				if (value != this.lastWorkItemsUpdate)
				{
					this.lastWorkItemsUpdate = value;
					AsyncRaisePropertyChanged("LastUpdate");
				}
			}
		}

		#endregion

		#region Event Handlers

		private void workItemStore_WorkItemsRefreshed(object sender, RefreshResultsEventArgs e)
		{
			this.logger.Info("Work item store refreshed.");

			LastRefresh = SystemTime.Now();

			if (e.Exception != null)
			{
				this.logger.Info(e.Exception, "Exception found during a WorkItemStore request.");
			}

			Exception = e.Exception;
		}

		private void workItemStore_WorkItemsUpdated(object sender, EventArgs e)
		{
			this.logger.Info("Found updates in the work item store.");

			Debug.Assert(Exception == null);

			ProcessWorkItems();
		}

		private void teamInfoProvider_PropertyChanged(object sender, PropertyChangedEventArgs e)
		{
			this.unassignedTeamMember = null;
			UnassignedWork = null;
			TeamMembers.Clear();

			RefreshData(false, true);
		}

		#endregion

		#region Implementation of INotifyPropertyChanged

		public event PropertyChangedEventHandler PropertyChanged;

		private void RaisePropertyChanged(object state)
		{
			if (PropertyChanged != null)
			{
				string propertyName = (string) state;
				PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
			}
		}

		private void AsyncRaisePropertyChanged(string propertyName)
		{
			if (this.syncContext == null || !Thread.CurrentThread.IsBackground)
			{
				RaisePropertyChanged(propertyName);
			}
			else
			{
				this.syncContext.Post(RaisePropertyChanged, propertyName);
			}
		}

		#endregion

		#region Implementation of IDisposable

		public void Dispose()
		{
			if (!this.disposed)
			{
				this.disposed = true;

				this.logger.Info("Sprint object has been disposed.");

				if (this.timer != null)
				{
					this.timer.Change(Timeout.Infinite, Timeout.Infinite);
					this.timer.Dispose();
				}

				var disposable = this.availableTimeCalculator as IDisposable;
				if (disposable != null)
				{
					disposable.Dispose();
				}

				if (this.teamInfoProviderSubscribed)
				{
					this.teamInfoProvider.PropertyChanged -= teamInfoProvider_PropertyChanged;
					this.teamInfoProviderSubscribed = false;
				}
			}
		}

		#endregion
	}
}